<template>
  <div>
    <el-button type="primary" @click="openDialog">
      导出
    </el-button>

    <el-dialog :title="`导出表格 (共${tableData.length}条数据)`" v-model="dialogVisible" width="400px" @close="resetFileName"
      append-to-body>
      <div class="dialog-content">
        <div class="file-name-input">
          <el-input v-model="fileName" placeholder="请输入文件名" clearable>
            <template #append>.xlsx</template>
          </el-input>
        </div>
      </div>

      <template #footer>
        <el-button @click="dialogVisible = false">取消</el-button>
        <el-button type="primary" @click="confirmExport" :loading="exporting">确认导出</el-button>
      </template>
    </el-dialog>

    <el-table :data="tableData" border style="width: 800px; margin-top: 16px;" :span-method="spanMethod" ref="tableRef"
      v-loading="exporting" element-loading-text="正在导出...">
      <!-- 多级表头 -->
      <el-table-column label="基本信息" align="center">
        <el-table-column prop="name" label="姓名" width="120" />
        <el-table-column prop="age" label="年龄" width="80" />
      </el-table-column>

      <el-table-column label="联系方式" align="center">
        <el-table-column prop="phone" label="电话" width="150" />
        <el-table-column prop="email" label="邮箱" width="200" />
      </el-table-column>

      <el-table-column prop="address" label="地址" />
    </el-table>
  </div>
</template>

<script setup>
import { ref, onMounted } from 'vue'
import { ElMessage } from 'element-plus'
import * as XLSX from 'xlsx'
import { saveAs } from 'file-saver'

// 对话框显示状态
const dialogVisible = ref(false)
// 文件名
const fileName = ref('表格导出')
// 导出加载状态
const exporting = ref(false)
// 表格引用
const tableRef = ref()

// 表格数据
const tableData = ref([
  { name: '张三', age: 28, phone: '13800000000', email: 'zhangsan@example.com', address: '北京市朝阳区' },
  { name: '张三', age: 28, phone: '13800000001', email: 'zhangsan2@example.com', address: '北京市海淀区' },
  { name: '李四', age: 32, phone: '13900000000', email: 'lisi@example.com', address: '上海市浦东新区' },
  { name: '李四', age: 32, phone: '13900000001', email: 'lisi2@example.com', address: '上海市徐汇区' },
  { name: '王五', age: 40, phone: '13700000000', email: 'wangwu@example.com', address: '广州市天河区' },
  { name: '王五', age: 40, phone: '13700000001', email: 'wangwu2@example.com', address: '广州市越秀区' },
])

// 存储合并单元格信息
const mergeInfo = ref({})

onMounted(() => {
  // 预计算合并信息
  calculateMergeInfo()
})

/**
 * 计算合并单元格信息
 * 遍历表格数据，找出需要合并的单元格并记录合并信息
 */
function calculateMergeInfo() {
  const info = {}

  // 遍历表格数据
  tableData.value.forEach((row, rowIndex) => {
    // 只对姓名为"王五"的行进行处理
    if (row.name === '王五') {
      // 每隔一行进行合并（合并两行）
      if (rowIndex % 2 === 0) {
        // 记录姓名列的合并信息：跨2行1列
        info[`${rowIndex}-name`] = { rowspan: 2, colspan: 1 }
        // 记录年龄列的合并信息：跨2行1列
        info[`${rowIndex}-age`] = { rowspan: 2, colspan: 1 }
        // 下一行的姓名列隐藏（因为已经被合并）
        info[`${rowIndex + 1}-name`] = { rowspan: 0, colspan: 0 }
        // 下一行的年龄列隐藏（因为已经被合并）
        info[`${rowIndex + 1}-age`] = { rowspan: 0, colspan: 0 }
      }
    }
  })

  // 存储合并信息
  mergeInfo.value = info
}

/**
 * 表格合并单元格逻辑
 * @param {Object} param - 包含行、列、行索引、列索引的对象
 * @returns {Array} 返回[rowspan, colspan]数组
 */
function spanMethod({ row, column, rowIndex, columnIndex }) {
  // 生成唯一键，格式为"行索引-列属性名"
  const key = `${rowIndex}-${column.property}`
  // 如果存在合并信息，返回对应的行列跨度
  if (mergeInfo.value[key]) {
    return [mergeInfo.value[key].rowspan, mergeInfo.value[key].colspan]
  }
  // 默认不合并
  return [1, 1]
}

/**
 * 打开导出对话框
 */
const openDialog = () => {
  dialogVisible.value = true
}

/**
 * 重置文件名为默认值
 * 在对话框关闭时调用
 */
function resetFileName() {
  fileName.value = '表格导出'
}

/**
 * 确认导出，校验文件名有效性
 * 验证文件名后执行导出操作
 */
async function confirmExport() {
  // 检查文件名是否为空
  if (!fileName.value.trim()) {
    ElMessage.warning('请输入有效的文件名')
    return
  }

  try {
    // 开始导出，显示加载状态
    exporting.value = true
    // 执行导出操作
    await exportExcel()
    // 导出成功，关闭对话框
    dialogVisible.value = false
    // 显示成功消息
    ElMessage.success('导出成功')
  } catch (error) {
    // 导出失败，记录错误并提示用户
    console.error('导出失败:', error)
    ElMessage.error('导出失败，请重试')
  } finally {
    // 无论成功失败，都关闭加载状态
    exporting.value = false
  }
}

/**
 * 导出 Excel 文件，支持多级表头和合并单元格
 * 使用xlsx库生成Excel文件，file-saver保存文件
 */
async function exportExcel() {
  try {
    // ==================== 构建表头 ====================
    // 多级表头，使用二维数组表示多行表头
    // 第一行：分组标题
    // 第二行：具体列标题
    const header = [
      ['基本信息', '基本信息', '联系方式', '联系方式', '地址'], // 分组行
      ['姓名', '年龄', '电话', '邮箱', '地址'], // 列标题行
    ]

    // ==================== 构建数据 ====================
    // 将表格数据映射为二维数组格式，便于xlsx处理
    const data = tableData.value.map(row => [
      row.name,      // 姓名列
      row.age,       // 年龄列
      row.phone,     // 电话列
      row.email,     // 邮箱列
      row.address,   // 地址列
    ])

    // ==================== 生成工作表 ====================
    // 将表头和数据合并成一个二维数组，并转换为工作表
    const ws = XLSX.utils.aoa_to_sheet([...header, ...data])

    // ==================== 设置样式和合并单元格 ====================
    // 检查工作表是否有数据范围
    if (ws['!ref']) {
      // 解码工作表的数据范围（如A1:E8）
      const range = XLSX.utils.decode_range(ws['!ref'])

      // 设置表头样式（前两行）
      for (let r = 0; r < 2; r++) {        // 遍历前两行（表头行）
        for (let c = 0; c < 5; c++) {      // 遍历所有列
          // 获取单元格地址（如A1、B1等）
          const cellAddress = XLSX.utils.encode_cell({ r, c })
          // 如果单元格不存在，创建空对象
          if (!ws[cellAddress]) ws[cellAddress] = {}
          // 如果样式对象不存在，创建空对象
          if (!ws[cellAddress].s) ws[cellAddress].s = {}

          // 设置表头单元格样式
          ws[cellAddress].s = {
            font: { bold: true }, // 粗体
            alignment: { horizontal: 'center', vertical: 'center' }, // 居中
            fill: { fgColor: { rgb: "E6E6FA" } } // 浅紫色背景
          }
        }
      }

      // ==================== 设置合并单元格 ====================
      // 初始化合并区域数组
      const merges = [
        // 合并表头：基本信息分组（合并前两列）
        { s: { r: 0, c: 0 }, e: { r: 0, c: 1 } },
        // 合并表头：联系方式分组（合并第三、四列）
        { s: { r: 0, c: 2 }, e: { r: 0, c: 3 } },
      ]

      // 处理数据行的合并单元格
      Object.keys(mergeInfo.value).forEach(key => {
        // 只处理需要跨多行的合并
        if (mergeInfo.value[key].rowspan > 1) {
          // 解析键名，获取行索引和列名
          const [rowIndex, colName] = key.split('-')
          // 根据列名确定列索引：姓名列为0，年龄列为1
          const colIndex = colName === 'name' ? 0 : 1

          // 添加合并区域（+2是因为有2行表头）
          merges.push({
            // 起始位置：数据行索引+表头行数，对应列
            s: { r: parseInt(rowIndex) + 2, c: colIndex },
            // 结束位置：起始行+合并行数-1，同一列
            e: { r: parseInt(rowIndex) + 2 + mergeInfo.value[key].rowspan - 1, c: colIndex }
          })
        }
      })

      // 将合并区域设置到工作表
      ws['!merges'] = merges
    }

    // ==================== 生成工作簿并导出 ====================
    // 创建新的工作簿
    const wb = XLSX.utils.book_new()
    // 将工作表添加到工作簿，命名为'Sheet1'
    XLSX.utils.book_append_sheet(wb, ws, 'Sheet1')

    // 将工作簿写入ArrayBuffer
    const wbout = XLSX.write(wb, {
      bookType: 'xlsx', // 文件类型为xlsx
      type: 'array'     // 输出类型为数组
    })

    // 创建Blob对象（二进制大对象）
    const blob = new Blob([wbout], {
      type: 'application/octet-stream' // 设置MIME类型
    })

    // ==================== 生成文件名并保存 ====================
    // 获取当前日期，格式为YYYYMMDD
    const dateStr = new Date().toISOString().slice(0, 10).replace(/-/g, '')
    // 组合最终文件名：日期_用户输入的文件名.xlsx
    const finalFileName = `${dateStr}_${fileName.value.trim()}.xlsx`

    // 使用file-saver保存文件
    saveAs(blob, finalFileName)
  } catch (error) {
    // 捕获并抛出错误，由调用方处理
    console.error('导出Excel出错:', error)
    throw new Error('导出失败')
  }
}
</script>

<style scoped>
.dialog-content {
  padding: 10px 0;
}

.file-name-input {
  margin-bottom: 15px;
}
</style>